package client;

import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;

import javax.swing.JOptionPane;

import util.Util;

import client.view.MasterServerView;
import net.GameInfo;

/**
 * Communicates with the MasterServer to receive updates on the state of the game lobby,
 * add game tables, and join game tables.  Point-of-entry for client-side execution.
 * LobbyClient is a singleton to facilitate references to its member functions.
 * While LobbyClient has a connectToTable method, it has no leaveTable method because this
 * functionality is handled by the Client-GameServer connection - the client leaves the
 * table when it interrupts the connection. 
 */
public class LobbyClient
{
	/**
	 * Start a new LobbyClient.  The LobbyClient begins listening for table updates from 
	 * the server.<br>
	 * Usage: java client.LobbyClient masterServerAddress masterServerPort
	 * @param args the command-line arguments
	 * @throws NumberFormatException
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public static void main(String[] args) {
		if (args.length > 4)
			quitWithError(USAGE_STRING, 1);

		try {
			new LobbyClient(InetAddress.getByName(args[0]), Integer.parseInt(args[1])).processInput();
		} catch (ArrayIndexOutOfBoundsException e) {
			Util.quitWithError(USAGE_STRING, 1);
		} catch (NumberFormatException e) {
			Util.quitWithError(USAGE_STRING, 1);
		} catch (java.net.UnknownHostException e) {
			Util.quitWithError("Unknown host: " + args[0], 3);
		} catch (LoginException e) {
			Util.quitWithError("Login failed: " + e.getMessage(), 4);
		} catch (Exception e) {
			Util.quitWithError("An unexpected exception occurred in LobbyClient: " + e.getMessage(), 2);
		}
	}

	/** 
	 * Sends a message to a player logged onto the MasterServer.
	 * @param to a proxy for the player's Account
	 * @param message the message to be sent
	 */
	public void sendMessage(String to, String message) throws IOException {
		connection.println("privateMessage");
		connection.println(to);
		connection.println(message);
	}
	
	/**
	 * Sends a message to the MasterServer's general chat.
	 *
	 */
	public void sendLobbyMessage(String message) throws IOException {
		connection.println("lobbyMessage");
		connection.println(message);
	}

	public void sendGuiClosed(String who) throws IOException {
		connection.println("guiClosed");
		connection.println(who);
	}

	/**
	 * Connect to the Master Server.  
	 * @param username the username for the account
	 * @param password the password for the account
	 */
	public AccountProxy login(LoginInfo li) throws IOException, LoginException {
		connection.println(li.getUsername());
		connection.println(li.getPassword());
		if ("welcome".equals(connection.readLine()))
			return new AccountProxy(li.getUsername());
		else
			throw new LoginException("Bad user name or password, or account already logged in");
		//return "welcome".equals(connection.readLine()) ? new AccountProxy(li.getUsername()) : null;
	}
	
	/**
	 * Returns the list of accounts (stood in for by AccountProxies) connected to the master server
	 */
	public java.util.Collection<AccountProxy> getLoggedOn() {
		return java.util.Collections.unmodifiableCollection(loggedOn.values());
	}

	public java.util.Collection<GameInfo> getTables() {
		return java.util.Collections.unmodifiableCollection(tables.values());
	}

	/**
	 * Listens for information from the MasterServer as tables become available and 
	 * accounts become available.  This method is constantly executing while the 
	 * LobbyClient is active.
	 * @param host the MasterServer's hostname
	 * @param port the MasterServer's port number
	 */
	private void processInput() throws IOException
	{
		System.out.println("ready to read client input");
		String line;
		while ((line = connection.readLine()) != null) {
			System.out.println(line);
			if ("tableAdded".equals(line))
				tableAdded(Integer.parseInt(connection.readLine()), readGameInfo());
			else if ("tableRemoved".equals(line))
				tableRemoved(Integer.parseInt(connection.readLine()));
			else if ("clientAdded".equals(line))
				clientAdded(connection.readLine());
			else if ("clientRemoved".equals(line))
				clientRemoved(connection.readLine());
			else if ("lobbyMessage".equals(line))
				readLobbyMessage(connection.readLine() + ":  " + connection.readLine());
			else if ("privateMessage".equals(line))
				readPrivateMessage(connection.readLine() + ":  " + connection.readLine());
		}
	}

	private void readLobbyMessage(String msg) {
		pcs.firePropertyChange("lobbyMessage",null,msg);
	}

	private void readPrivateMessage(String msg) {
		pcs.firePropertyChange("privateMessage",null,msg);
	}

	/**
	 * Removes a table from the set of tables visible to this LobbyClient.
	 * @param index the table's index in the MasterServer's table set
	 */
	private void tableRemoved(int index) {
		pcs.firePropertyChange("removeTable",tables.get(index),null);
		tables.remove(index);
	}

	/**
	 * Adds a table to the set of tables visible to this LobbyClient. 
	 * @param in the BufferedReader constructed from the MasterServer-LobbyClient socket stream
	 * (for details on the protocol used, please see server/MasterServer)
	 * @throws IOException
	 */
	private void tableAdded(int tableNum, GameInfo gameInfo) throws IOException {
		tables.put(tableNum, gameInfo);
		pcs.firePropertyChange("addTable", null, gameInfo);
	}

	private void clientAdded(String username) {
		AccountProxy ap = new AccountProxy(username);
		loggedOn.put(username, ap);
		pcs.firePropertyChange("addClient", null, ap);
	}

	private void clientRemoved(String username) {
		loggedOn.remove(username);
		pcs.firePropertyChange("removeClient",new AccountProxy(username),null);
	}

	private GameInfo readGameInfo() throws IOException {
		InetAddress gameHost = InetAddress.getByName(connection.readLine());
		System.out.println("GameInfo host " + gameHost);
		int gamePort = Integer.parseInt(connection.readLine());
		String module = connection.readLine();
		int numPlayers = Integer.parseInt(connection.readLine());
		String line;
		java.util.Map<String, String> options = new java.util.HashMap<String, String>();
		util.Pair<String, String> option;
		while ((line = connection.readLine()) != null && line.length() > 0)
			options.put((option = Util.splitOnFirstOccurrence(line, '=')).first, option.second);
		return new GameInfo(gameHost, gamePort, module, numPlayers, options);
	}

	private LoginInfo getLogIn() {
		String username = null, password = null;
		do
			username=JOptionPane.showInputDialog("Username:");
		while (username != null && username.length() == 0);
		if (username != null) {
			do
				password=JOptionPane.showInputDialog("Password:");
			while (password != null && password.length() == 0);
			if (password != null)
				return new LoginInfo(username,password);
		}
		return null;
	}

	/** 
	 * Private empty constructor to enforce Singleton pattern
	 */
	private LobbyClient(InetAddress host, int port) throws IOException, LoginException {
		System.out.println("Connecting to master server at " + host + ":" + port + "...");
		connection = new net.Connection(new Socket(host, port));
		LoginInfo li = getLogIn();
		if (li == null)
			System.exit(0);
		login(li);

/*		boolean loggedIn=false;
		while (!loggedIn) {
			li = getLogIn();
			try { 
				login(li);
				loggedIn=true;
			} catch(LoginException le) {
				String msg="Username: " + li.getUsername() + ", Password: " + li.getPassword() + " is invalid.";
				int i=JOptionPane.showConfirmDialog(null, msg + "\nDo you want to try again?", "Login Error", JOptionPane.YES_NO_OPTION);
				if (i==JOptionPane.NO_OPTION || i==JOptionPane.CLOSED_OPTION) {
					connection.println("cancel_login");
					System.exit(-1);
				}
			}
			System.out.println("looping");
		}
*/
		pcs = new PropertyChangeSupport(this);
		MasterServerView msv = new MasterServerView(this, new AccountProxy(li.getUsername()));
		pcs.addPropertyChangeListener(msv);
	}

	private static void quitWithError(String message, int errorNum) {
		System.err.println(message);
		System.exit(errorNum);
	}

	private static class LoginInfo {
		private LoginInfo(String u, String p) { this.username=u; this.password=p; }
		private String getUsername() { return username; }
		private String getPassword() { return password; }
		private final String username, password;
	}

	/** A set of tables present on the MasterServer.  Tables are indexed by integers. */
	private final java.util.Map<Integer, GameInfo> tables = new java.util.HashMap<Integer, GameInfo>();
	/** A list of user account proxies for the users logged into the dedicated server */
	private final java.util.Map<String, AccountProxy> loggedOn = new java.util.HashMap<String, AccountProxy>();
	private final net.Connection connection;
	private PropertyChangeSupport pcs;
	private static final String USAGE_STRING = "Usage: java client.LobbyClient masterServerAddress masterServerPort[ username[ password]]";
}